summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbunnei <bunneidev@gmail.com>2023-02-04 01:13:16 +0100
committerbunnei <bunneidev@gmail.com>2023-06-03 09:05:29 +0200
commitef605f7d8f8241b95b977d95cf5247c1f2d8a309 (patch)
tree7e9dcc62168e23115d05119a5854d59544c89b8d
parentandroid: Harden emulation shutdown when loader fails. (diff)
downloadyuzu-ef605f7d8f8241b95b977d95cf5247c1f2d8a309.tar
yuzu-ef605f7d8f8241b95b977d95cf5247c1f2d8a309.tar.gz
yuzu-ef605f7d8f8241b95b977d95cf5247c1f2d8a309.tar.bz2
yuzu-ef605f7d8f8241b95b977d95cf5247c1f2d8a309.tar.lz
yuzu-ef605f7d8f8241b95b977d95cf5247c1f2d8a309.tar.xz
yuzu-ef605f7d8f8241b95b977d95cf5247c1f2d8a309.tar.zst
yuzu-ef605f7d8f8241b95b977d95cf5247c1f2d8a309.zip
-rw-r--r--src/android/app/build.gradle6
-rw-r--r--src/android/app/src/main/AndroidManifest.xml13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java22
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java38
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java120
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java52
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.java28
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java83
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java132
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.java125
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java65
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java264
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java35
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java42
-rw-r--r--src/android/app/src/main/jni/config.cpp29
-rw-r--r--src/android/app/src/main/jni/id_cache.cpp46
-rw-r--r--src/android/app/src/main/jni/native.cpp12
-rw-r--r--src/android/app/src/main/jni/native.h89
-rw-r--r--src/android/app/src/main/res/layout/filepicker_toolbar.xml32
-rw-r--r--src/android/app/src/main/res/values-night/styles_filepicker.xml5
-rw-r--r--src/android/app/src/main/res/values-w1050dp/dimens.xml1
-rw-r--r--src/android/app/src/main/res/values-w820dp/dimens.xml1
-rw-r--r--src/android/app/src/main/res/values/strings.xml3
-rw-r--r--src/android/app/src/main/res/values/styles.xml16
-rw-r--r--src/android/app/src/main/res/values/styles_filepicker.xml5
-rw-r--r--src/common/CMakeLists.txt8
-rw-r--r--src/common/fs/file.cpp38
-rw-r--r--src/common/fs/fs_android.cpp98
-rw-r--r--src/common/fs/fs_android.h62
-rw-r--r--src/common/fs/path_util.cpp31
-rw-r--r--src/common/fs/path_util.h8
38 files changed, 851 insertions, 697 deletions
diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle
index ffbadce14..c516b2bff 100644
--- a/src/android/app/build.gradle
+++ b/src/android/app/build.gradle
@@ -32,7 +32,7 @@ android {
// TODO If this is ever modified, change application_id in strings.xml
applicationId "org.yuzu.yuzu_emu"
minSdkVersion 28
- targetSdkVersion 29
+ targetSdkVersion 31
versionCode autoVersion
versionName getVersion()
ndk.abiFilters abiFilter
@@ -126,6 +126,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
implementation 'androidx.fragment:fragment:1.5.3'
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
+ implementation "androidx.documentfile:documentfile:1.0.1"
implementation 'com.google.android.material:material:1.6.1'
// For loading huge screenshots from the disk.
@@ -138,9 +139,6 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
-
- // Please don't upgrade the billing library as the newer version is not GPL-compatible
- implementation 'com.android.billingclient:billing:2.0.3'
}
def getVersion() {
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 0d7e3f7ad..88e1669cd 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -31,6 +31,7 @@
<activity
android:name="org.yuzu.yuzu_emu.ui.main.MainActivity"
+ android:exported="true"
android:theme="@style/YuzuBase"
android:resizeableActivity="false">
@@ -57,18 +58,6 @@
<service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/>
- <activity
- android:name="org.yuzu.yuzu_emu.activities.CustomFilePickerActivity"
- android:label="@string/app_name"
- android:theme="@style/FilePickerTheme">
- <intent-filter>
- <action android:name="android.intent.action.GET_CONTENT" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
-
- <service android:name="org.yuzu.yuzu_emu.utils.DirectoryInitialization"/>
-
<provider
android:name="org.yuzu.yuzu_emu.model.GameProvider"
android:authorities="${applicationId}.provider"
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java
index e15612a36..acb3fc2d6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java
@@ -25,7 +25,9 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import org.yuzu.yuzu_emu.activities.EmulationActivity;
+import org.yuzu.yuzu_emu.utils.DocumentsTree;
import org.yuzu.yuzu_emu.utils.EmulationMenuSettings;
+import org.yuzu.yuzu_emu.utils.FileUtil;
import org.yuzu.yuzu_emu.utils.Log;
import java.lang.ref.WeakReference;
@@ -66,6 +68,20 @@ public final class NativeLibrary {
// Disallows instantiation.
}
+ public static int openContentUri(String path, String openmode) {
+ if (DocumentsTree.isNativePath(path)) {
+ return YuzuApplication.documentsTree.openContentUri(path, openmode);
+ }
+ return FileUtil.openContentUri(YuzuApplication.getAppContext(), path, openmode);
+ }
+
+ public static long getSize(String path) {
+ if (DocumentsTree.isNativePath(path)) {
+ return YuzuApplication.documentsTree.getFileSize(path);
+ }
+ return FileUtil.getFileSize(YuzuApplication.getAppContext(), path);
+ }
+
/**
* Handles button press events for a gamepad.
*
@@ -147,11 +163,7 @@ public final class NativeLibrary {
public static native String GetGitRevision();
- /**
- * Sets the current working user directory
- * If not set, it auto-detects a location
- */
- public static native void SetUserDirectory(String directory);
+ public static native void SetAppDirectory(String directory);
// Create the config.ini file.
public static native void CreateConfigFile();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java
index 700916f87..d7b75e5a6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java
@@ -11,11 +11,12 @@ import android.content.Context;
import android.os.Build;
import org.yuzu.yuzu_emu.model.GameDatabase;
+import org.yuzu.yuzu_emu.utils.DocumentsTree;
import org.yuzu.yuzu_emu.utils.DirectoryInitialization;
-import org.yuzu.yuzu_emu.utils.PermissionsHandler;
public class YuzuApplication extends Application {
public static GameDatabase databaseHelper;
+ public static DocumentsTree documentsTree;
private static YuzuApplication application;
private void createNotificationChannel() {
@@ -39,10 +40,9 @@ public class YuzuApplication extends Application {
public void onCreate() {
super.onCreate();
application = this;
+ documentsTree = new DocumentsTree();
- if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
- DirectoryInitialization.start(getApplicationContext());
- }
+ DirectoryInitialization.start(getApplicationContext());
NativeLibrary.LogDeviceInfo();
createNotificationChannel();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java
deleted file mode 100644
index a79780814..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.yuzu.yuzu_emu.activities;
-
-import android.content.Intent;
-import android.os.Environment;
-
-import androidx.annotation.Nullable;
-
-import com.nononsenseapps.filepicker.AbstractFilePickerFragment;
-import com.nononsenseapps.filepicker.FilePickerActivity;
-
-import org.yuzu.yuzu_emu.fragments.CustomFilePickerFragment;
-
-import java.io.File;
-
-public class CustomFilePickerActivity extends FilePickerActivity {
- public static final String EXTRA_TITLE = "filepicker.intent.TITLE";
- public static final String EXTRA_EXTENSIONS = "filepicker.intent.EXTENSIONS";
-
- @Override
- protected AbstractFilePickerFragment<File> getFragment(
- @Nullable final String startPath, final int mode, final boolean allowMultiple,
- final boolean allowCreateDir, final boolean allowExistingFile,
- final boolean singleClick) {
- CustomFilePickerFragment fragment = new CustomFilePickerFragment();
- // startPath is allowed to be null. In that case, default folder should be SD-card and not "/"
- fragment.setArgs(
- startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(),
- mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick);
-
- Intent intent = getIntent();
- int title = intent == null ? 0 : intent.getIntExtra(EXTRA_TITLE, 0);
- fragment.setTitle(title);
- String allowedExtensions = intent == null ? "*" : intent.getStringExtra(EXTRA_EXTENSIONS);
- fragment.setAllowedExtensions(allowedExtensions);
-
- return fragment;
- }
-}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
index fa785741b..cd9f823d4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
@@ -16,16 +16,16 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
+import org.yuzu.yuzu_emu.YuzuApplication;
import org.yuzu.yuzu_emu.R;
import org.yuzu.yuzu_emu.activities.EmulationActivity;
import org.yuzu.yuzu_emu.model.GameDatabase;
import org.yuzu.yuzu_emu.ui.DividerItemDecoration;
+import org.yuzu.yuzu_emu.utils.FileUtil;
import org.yuzu.yuzu_emu.utils.Log;
import org.yuzu.yuzu_emu.utils.PicassoUtils;
import org.yuzu.yuzu_emu.viewholders.GameViewHolder;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.stream.Stream;
/**
@@ -88,8 +88,9 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
- final Path gamePath = Paths.get(mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
- holder.textFileName.setText(gamePath.getFileName().toString());
+ String filepath = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
+ String filename = FileUtil.getFilename(YuzuApplication.getAppContext(), filepath);
+ holder.textFileName.setText(filename);
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java
index 916ced382..0a1323a1f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java
@@ -160,12 +160,6 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
}
@Override
- public void showPermissionNeededHint() {
- Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
- .show();
- }
-
- @Override
public void showExternalStorageNotMountedHint() {
Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT)
.show();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java
index ba6b6762b..25b7758a9 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java
@@ -78,9 +78,6 @@ public final class SettingsActivityPresenter {
if (directoryInitializationState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
mView.hideLoading();
loadSettingsUI();
- } else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
- mView.showPermissionNeededHint();
- mView.hideLoading();
} else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
mView.showExternalStorageNotMountedHint();
mView.hideLoading();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java
index 5aff3bcf7..58ccf31b7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java
@@ -77,11 +77,6 @@ public interface SettingsActivityView {
void hideLoading();
/**
- * Show a hint to the user that the app needs write to external storage access
- */
- void showPermissionNeededHint();
-
- /**
* Show a hint to the user that the app needs the external storage to be mounted
*/
void showExternalStorageNotMountedHint();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java
deleted file mode 100644
index 2658b1445..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package org.yuzu.yuzu_emu.fragments;
-
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Environment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.widget.Toolbar;
-import androidx.core.content.FileProvider;
-
-import com.nononsenseapps.filepicker.FilePickerFragment;
-
-import org.yuzu.yuzu_emu.R;
-
-import java.io.File;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-public class CustomFilePickerFragment extends FilePickerFragment {
- private static String ALL_FILES = "*";
- private int mTitle;
- private static List<String> extensions = Collections.singletonList(ALL_FILES);
-
- @NonNull
- @Override
- public Uri toUri(@NonNull final File file) {
- return FileProvider
- .getUriForFile(getContext(),
- getContext().getApplicationContext().getPackageName() + ".filesprovider",
- file);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- if (mode == MODE_DIR) {
- TextView ok = getActivity().findViewById(R.id.nnf_button_ok);
- ok.setText(R.string.select_dir);
-
- TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel);
- cancel.setVisibility(View.GONE);
- }
- }
-
- @Override
- protected View inflateRootView(LayoutInflater inflater, ViewGroup container) {
- View view = super.inflateRootView(inflater, container);
- if (mTitle != 0) {
- Toolbar toolbar = view.findViewById(com.nononsenseapps.filepicker.R.id.nnf_picker_toolbar);
- ViewGroup parent = (ViewGroup) toolbar.getParent();
- int index = parent.indexOfChild(toolbar);
- View newToolbar = inflater.inflate(R.layout.filepicker_toolbar, toolbar, false);
- TextView title = newToolbar.findViewById(R.id.filepicker_title);
- title.setText(mTitle);
- parent.removeView(toolbar);
- parent.addView(newToolbar, index);
- }
- return view;
- }
-
- public void setTitle(int title) {
- mTitle = title;
- }
-
- public void setAllowedExtensions(String allowedExtensions) {
- if (allowedExtensions == null)
- return;
-
- extensions = Arrays.asList(allowedExtensions.split(","));
- }
-
- @Override
- protected boolean isItemVisible(@NonNull final File file) {
- // Some users jump to the conclusion that Dolphin isn't able to detect their
- // files if the files don't show up in the file picker when mode == MODE_DIR.
- // To avoid this, show files even when the user needs to select a directory.
- return (showHiddenItems || !file.isHidden()) &&
- (file.isDirectory() || extensions.contains(ALL_FILES) ||
- extensions.contains(fileExtension(file.getName()).toLowerCase()));
- }
-
- @Override
- public boolean isCheckable(@NonNull final File file) {
- // We need to make a small correction to the isCheckable logic due to
- // overriding isItemVisible to show files when mode == MODE_DIR.
- // AbstractFilePickerFragment always treats files as checkable when
- // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR.
- return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile());
- }
-
- @Override
- public void goUp() {
- if (Environment.getExternalStorageDirectory().getPath().equals(mCurrentPath.getPath())) {
- goToDir(new File("/storage/"));
- return;
- }
- if (mCurrentPath.equals(new File("/storage/"))){
- return;
- }
- super.goUp();
- }
-
- @Override
- public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) {
- if(viewHolder.file.equals(new File("/storage/emulated/")))
- viewHolder.file = new File("/storage/emulated/0/");
- super.onClickDir(view, viewHolder);
- }
-
- private static String fileExtension(@NonNull String filename) {
- int i = filename.lastIndexOf('.');
- return i < 0 ? "" : filename.substring(i + 1);
- }
-}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java
index f7a242171..32f077944 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java
@@ -156,10 +156,6 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
mEmulationState.run(activity.isActivityRecreated());
} else if (directoryInitializationState ==
- DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
- Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT)
- .show();
- } else if (directoryInitializationState ==
DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
Toast.makeText(getContext(), R.string.external_storage_not_mounted,
Toast.LENGTH_SHORT)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java
index ac5db1c36..771e35c69 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java
@@ -5,8 +5,10 @@ import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
import org.yuzu.yuzu_emu.NativeLibrary;
+import org.yuzu.yuzu_emu.utils.FileUtil;
import org.yuzu.yuzu_emu.utils.Log;
import java.io.File;
@@ -63,10 +65,12 @@ public final class GameDatabase extends SQLiteOpenHelper {
private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS;
private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
+ private final Context context;
public GameDatabase(Context context) {
// Superclass constructor builds a database or uses an existing one.
super(context, "games.db", null, DB_VERSION);
+ this.context = context;
}
@Override
@@ -123,8 +127,6 @@ public final class GameDatabase extends SQLiteOpenHelper {
File game = new File(gamePath);
if (!game.exists()) {
- Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " +
- gamePath);
database.delete(TABLE_NAME_GAMES,
KEY_DB_ID + " = ?",
new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))});
@@ -150,9 +152,9 @@ public final class GameDatabase extends SQLiteOpenHelper {
while (folderCursor.moveToNext()) {
String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
- File folder = new File(folderPath);
+ Uri folderUri = Uri.parse(folderPath);
// If the folder is empty because it no longer exists, remove it from the library.
- if (!folder.exists()) {
+ if (FileUtil.listFiles(context, folderUri).length == 0) {
Log.error(
"[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
database.delete(TABLE_NAME_FOLDERS,
@@ -160,7 +162,7 @@ public final class GameDatabase extends SQLiteOpenHelper {
new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
}
- addGamesRecursive(database, folder, allowedExtensions, 3);
+ this.addGamesRecursive(database, folderUri, allowedExtensions, 3);
}
fileCursor.close();
@@ -169,33 +171,27 @@ public final class GameDatabase extends SQLiteOpenHelper {
database.close();
}
- private static void addGamesRecursive(SQLiteDatabase database, File parent, Set<String> allowedExtensions, int depth) {
+ private void addGamesRecursive(SQLiteDatabase database, Uri parent, Set<String> allowedExtensions, int depth) {
if (depth <= 0) {
return;
}
- File[] children = parent.listFiles();
- if (children != null) {
- for (File file : children) {
- if (file.isHidden()) {
- continue;
- }
-
- if (file.isDirectory()) {
- Set<String> newExtensions = new HashSet<>(Arrays.asList(
- ".xci", ".nsp", ".nca", ".nro"));
- addGamesRecursive(database, file, newExtensions, depth - 1);
- } else {
- String filePath = file.getPath();
-
- int extensionStart = filePath.lastIndexOf('.');
- if (extensionStart > 0) {
- String fileExtension = filePath.substring(extensionStart);
-
- // Check that the file has an extension we care about before trying to read out of it.
- if (allowedExtensions.contains(fileExtension.toLowerCase())) {
- attemptToAddGame(database, filePath);
- }
+ MinimalDocumentFile[] children = FileUtil.listFiles(context, parent);
+ for (MinimalDocumentFile file : children) {
+ if (file.isDirectory()) {
+ Set<String> newExtensions = new HashSet<>(Arrays.asList(
+ ".xci", ".nsp", ".nca", ".nro"));
+ this.addGamesRecursive(database, file.getUri(), newExtensions, depth - 1);
+ } else {
+ String filename = file.getUri().toString();
+
+ int extensionStart = filename.lastIndexOf('.');
+ if (extensionStart > 0) {
+ String fileExtension = filename.substring(extensionStart);
+
+ // Check that the file has an extension we care about before trying to read out of it.
+ if (allowedExtensions.contains(fileExtension.toLowerCase())) {
+ attemptToAddGame(database, filename);
}
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.java
new file mode 100644
index 000000000..4ec001a7f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.java
@@ -0,0 +1,28 @@
+package org.yuzu.yuzu_emu.model;
+
+import android.net.Uri;
+import android.provider.DocumentsContract;
+
+public class MinimalDocumentFile {
+ private final String filename;
+ private final Uri uri;
+ private final String mimeType;
+
+ public MinimalDocumentFile(String filename, String mimeType, Uri uri) {
+ this.filename = filename;
+ this.mimeType = mimeType;
+ this.uri = uri;
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public Uri getUri() {
+ return uri;
+ }
+
+ public boolean isDirectory() {
+ return mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java
index d419750a3..26ff14914 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java
@@ -1,12 +1,11 @@
package org.yuzu.yuzu_emu.ui.main;
import android.content.Intent;
-import android.content.pm.PackageManager;
+import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
-import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
@@ -18,16 +17,11 @@ import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity;
import org.yuzu.yuzu_emu.model.GameProvider;
import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment;
import org.yuzu.yuzu_emu.utils.AddDirectoryHelper;
-import org.yuzu.yuzu_emu.utils.DirectoryInitialization;
import org.yuzu.yuzu_emu.utils.FileBrowserHelper;
-import org.yuzu.yuzu_emu.utils.PermissionsHandler;
import org.yuzu.yuzu_emu.utils.PicassoUtils;
import org.yuzu.yuzu_emu.utils.StartupHandler;
import org.yuzu.yuzu_emu.utils.ThemeUtil;
-import java.util.Arrays;
-import java.util.Collections;
-
/**
* The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which
* individually display a grid of available games for each Fragment, in a tabbed layout.
@@ -54,12 +48,9 @@ public final class MainActivity extends AppCompatActivity implements MainView {
mPresenter.onCreate();
if (savedInstanceState == null) {
- StartupHandler.HandleInit(this);
- if (PermissionsHandler.hasWriteAccess(this)) {
- mPlatformGamesFragment = new PlatformGamesFragment();
- getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
- .commit();
- }
+ StartupHandler.handleInit(this);
+ mPlatformGamesFragment = new PlatformGamesFragment();
+ getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment).commit();
} else {
mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment");
}
@@ -72,15 +63,13 @@ public final class MainActivity extends AppCompatActivity implements MainView {
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
- if (PermissionsHandler.hasWriteAccess(this)) {
- if (getSupportFragmentManager() == null) {
- return;
- }
- if (outState == null) {
- return;
- }
- getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
+ if (getSupportFragmentManager() == null) {
+ return;
+ }
+ if (outState == null) {
+ return;
}
+ getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
}
@Override
@@ -119,27 +108,17 @@ public final class MainActivity extends AppCompatActivity implements MainView {
@Override
public void launchSettingsActivity(String menuTag) {
- if (PermissionsHandler.hasWriteAccess(this)) {
- SettingsActivity.launch(this, menuTag, "");
- } else {
- PermissionsHandler.checkWritePermission(this);
- }
+ SettingsActivity.launch(this, menuTag, "");
}
@Override
public void launchFileListActivity(int request) {
- if (PermissionsHandler.hasWriteAccess(this)) {
- switch (request) {
- case MainPresenter.REQUEST_ADD_DIRECTORY:
- FileBrowserHelper.openDirectoryPicker(this,
- MainPresenter.REQUEST_ADD_DIRECTORY,
- R.string.select_game_folder,
- Arrays.asList("nso", "nro", "nca", "xci",
- "nsp", "kip"));
- break;
- }
- } else {
- PermissionsHandler.checkWritePermission(this);
+ switch (request) {
+ case MainPresenter.REQUEST_ADD_DIRECTORY:
+ FileBrowserHelper.openDirectoryPicker(this,
+ MainPresenter.REQUEST_ADD_DIRECTORY,
+ R.string.select_game_folder);
+ break;
}
}
@@ -155,6 +134,8 @@ public final class MainActivity extends AppCompatActivity implements MainView {
case MainPresenter.REQUEST_ADD_DIRECTORY:
// If the user picked a file, as opposed to just backing out.
if (resultCode == MainActivity.RESULT_OK) {
+ int takeFlags = (Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ getContentResolver().takePersistableUriPermission(Uri.parse(result.getDataString()), takeFlags);
// When a new directory is picked, we currently will reset the existing games
// database. This effectively means that only one game directory is supported.
// TODO(bunnei): Consider fixing this in the future, or removing code for this.
@@ -166,32 +147,6 @@ public final class MainActivity extends AppCompatActivity implements MainView {
}
}
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- switch (requestCode) {
- case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION:
- if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- DirectoryInitialization.start(this);
-
- mPlatformGamesFragment = new PlatformGamesFragment();
- getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
- .commit();
-
- // Immediately prompt user to select a game directory on first boot
- if (mPresenter != null) {
- mPresenter.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
- }
- } else {
- Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
- .show();
- }
- break;
- default:
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- break;
- }
- }
-
/**
* Called by the framework whenever any actionbar/toolbar icon is clicked.
*
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java
index 4cf643552..01f577600 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java
@@ -22,7 +22,7 @@ public final class MainPresenter {
public void onCreate() {
String versionName = BuildConfig.VERSION_NAME;
mView.setVersionString(versionName);
- refeshGameList();
+ refreshGameList();
}
public void launchFileListActivity(int request) {
@@ -63,7 +63,7 @@ public final class MainPresenter {
mDirToAdd = dir;
}
- public void refeshGameList() {
+ public void refreshGameList() {
GameDatabase databaseHelper = YuzuApplication.databaseHelper;
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
mView.refresh();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java
index bac52bb2a..f922ae183 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java
@@ -1,35 +1,16 @@
-/**
- * Copyright 2014 Dolphin Emulator Project
- * Licensed under GPLv2+
- * Refer to the license.txt file included.
- */
-
package org.yuzu.yuzu_emu.utils;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.Environment;
-import android.preference.PreferenceManager;
-
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.yuzu.yuzu_emu.NativeLibrary;
-import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicBoolean;
-/**
- * A service that spawns its own thread in order to copy several binary and shader files
- * from the yuzu APK to the external file system.
- */
public final class DirectoryInitialization {
public static final String BROADCAST_ACTION = "org.yuzu.yuzu_emu.BROADCAST";
-
public static final String EXTRA_STATE = "directoryState";
private static volatile DirectoryInitializationState directoryState = null;
private static String userPath;
@@ -37,7 +18,6 @@ public final class DirectoryInitialization {
public static void start(Context context) {
// Can take a few seconds to run, so don't block UI thread.
- //noinspection TrivialFunctionalExpressionUsage
((Runnable) () -> init(context)).run();
}
@@ -46,31 +26,15 @@ public final class DirectoryInitialization {
return;
if (directoryState != DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
- if (PermissionsHandler.hasWriteAccess(context)) {
- if (setUserDirectory()) {
- initializeInternalStorage(context);
- NativeLibrary.CreateConfigFile();
- directoryState = DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
- } else {
- directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
- }
- } else {
- directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
- }
+ initializeInternalStorage(context);
+ NativeLibrary.CreateConfigFile();
+ directoryState = DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
}
isDirectoryInitializationRunning.set(false);
sendBroadcastState(directoryState, context);
}
- private static void deleteDirectoryRecursively(File file) {
- if (file.isDirectory()) {
- for (File child : file.listFiles())
- deleteDirectoryRecursively(child);
- }
- file.delete();
- }
-
public static boolean areDirectoriesReady() {
return directoryState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
}
@@ -85,41 +49,13 @@ public final class DirectoryInitialization {
return userPath;
}
- private static native void SetSysDirectory(String path);
-
- private static boolean setUserDirectory() {
- if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
- File externalPath = Environment.getExternalStorageDirectory();
- if (externalPath != null) {
- userPath = externalPath.getAbsolutePath() + "/yuzu-emu";
- Log.debug("[DirectoryInitialization] User Dir: " + userPath);
- // NativeLibrary.SetUserDirectory(userPath);
- return true;
- }
-
- }
-
- return false;
- }
-
- private static void initializeInternalStorage(Context context) {
- File sysDirectory = new File(context.getFilesDir(), "Sys");
-
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
- String revision = NativeLibrary.GetGitRevision();
- if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) {
- // There is no extracted Sys directory, or there is a Sys directory from another
- // version of yuzu that might contain outdated files. Let's (re-)extract Sys.
- deleteDirectoryRecursively(sysDirectory);
- copyAssetFolder("Sys", sysDirectory, true, context);
-
- SharedPreferences.Editor editor = preferences.edit();
- editor.putString("sysDirectoryVersion", revision);
- editor.apply();
+ public static void initializeInternalStorage(Context context) {
+ try {
+ userPath = context.getExternalFilesDir(null).getCanonicalPath();
+ NativeLibrary.SetAppDirectory(userPath);
+ } catch(IOException e) {
+ e.printStackTrace();
}
-
- // Let the native code know where the Sys directory is.
- SetSysDirectory(sysDirectory.getPath());
}
private static void sendBroadcastState(DirectoryInitializationState state, Context context) {
@@ -129,58 +65,8 @@ public final class DirectoryInitialization {
LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
}
- private static void copyAsset(String asset, File output, Boolean overwrite, Context context) {
- Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output);
-
- try {
- if (!output.exists() || overwrite) {
- InputStream in = context.getAssets().open(asset);
- OutputStream out = new FileOutputStream(output);
- copyFile(in, out);
- in.close();
- out.close();
- }
- } catch (IOException e) {
- Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset +
- e.getMessage());
- }
- }
-
- private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite,
- Context context) {
- Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " +
- outputFolder);
-
- try {
- boolean createdFolder = false;
- for (String file : context.getAssets().list(assetFolder)) {
- if (!createdFolder) {
- outputFolder.mkdir();
- createdFolder = true;
- }
- copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file),
- overwrite, context);
- copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite,
- context);
- }
- } catch (IOException e) {
- Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder +
- e.getMessage());
- }
- }
-
- private static void copyFile(InputStream in, OutputStream out) throws IOException {
- byte[] buffer = new byte[1024];
- int read;
-
- while ((read = in.read(buffer)) != -1) {
- out.write(buffer, 0, read);
- }
- }
-
public enum DirectoryInitializationState {
YUZU_DIRECTORIES_INITIALIZED,
- EXTERNAL_STORAGE_PERMISSION_NEEDED,
CANT_FIND_EXTERNAL_STORAGE
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.java
new file mode 100644
index 000000000..beb790ab1
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.java
@@ -0,0 +1,125 @@
+package org.yuzu.yuzu_emu.utils;
+
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.annotation.Nullable;
+import androidx.documentfile.provider.DocumentFile;
+
+import org.yuzu.yuzu_emu.YuzuApplication;
+import org.yuzu.yuzu_emu.model.MinimalDocumentFile;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+public class DocumentsTree {
+ private DocumentsNode root;
+ private final Context context;
+ public static final String DELIMITER = "/";
+
+ public DocumentsTree() {
+ context = YuzuApplication.getAppContext();
+ }
+
+ public void setRoot(Uri rootUri) {
+ root = null;
+ root = new DocumentsNode();
+ root.uri = rootUri;
+ root.isDirectory = true;
+ }
+
+ public int openContentUri(String filepath, String openmode) {
+ DocumentsNode node = resolvePath(filepath);
+ if (node == null) {
+ return -1;
+ }
+ return FileUtil.openContentUri(context, node.uri.toString(), openmode);
+ }
+
+ public long getFileSize(String filepath) {
+ DocumentsNode node = resolvePath(filepath);
+ if (node == null || node.isDirectory) {
+ return 0;
+ }
+ return FileUtil.getFileSize(context, node.uri.toString());
+ }
+
+ public boolean Exists(String filepath) {
+ return resolvePath(filepath) != null;
+ }
+
+ @Nullable
+ private DocumentsNode resolvePath(String filepath) {
+ StringTokenizer tokens = new StringTokenizer(filepath, DELIMITER, false);
+ DocumentsNode iterator = root;
+ while (tokens.hasMoreTokens()) {
+ String token = tokens.nextToken();
+ if (token.isEmpty()) continue;
+ iterator = find(iterator, token);
+ if (iterator == null) return null;
+ }
+ return iterator;
+ }
+
+ @Nullable
+ private DocumentsNode find(DocumentsNode parent, String filename) {
+ if (parent.isDirectory && !parent.loaded) {
+ structTree(parent);
+ }
+ return parent.children.get(filename);
+ }
+
+ /**
+ * Construct current level directory tree
+ * @param parent parent node of this level
+ */
+ private void structTree(DocumentsNode parent) {
+ MinimalDocumentFile[] documents = FileUtil.listFiles(context, parent.uri);
+ for (MinimalDocumentFile document: documents) {
+ DocumentsNode node = new DocumentsNode(document);
+ node.parent = parent;
+ parent.children.put(node.name, node);
+ }
+ parent.loaded = true;
+ }
+
+ public static boolean isNativePath(String path) {
+ if (path.length() > 0) {
+ return path.charAt(0) == '/';
+ }
+ return false;
+ }
+
+ private static class DocumentsNode {
+ private DocumentsNode parent;
+ private final Map<String, DocumentsNode> children = new HashMap<>();
+ private String name;
+ private Uri uri;
+ private boolean loaded = false;
+ private boolean isDirectory = false;
+
+ private DocumentsNode() {}
+ private DocumentsNode(MinimalDocumentFile document) {
+ name = document.getFilename();
+ uri = document.getUri();
+ isDirectory = document.isDirectory();
+ loaded = !isDirectory;
+ }
+ private DocumentsNode(DocumentFile document, boolean isCreateDir) {
+ name = document.getName();
+ uri = document.getUri();
+ isDirectory = isCreateDir;
+ loaded = true;
+ }
+
+ private void rename(String name) {
+ if (parent == null) {
+ return;
+ }
+ parent.children.remove(this.name);
+ this.name = name;
+ parent.children.put(name, this);
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java
index ad3ec3dc1..6175f39c4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java
@@ -1,73 +1,16 @@
package org.yuzu.yuzu_emu.utils;
import android.content.Intent;
-import android.net.Uri;
-import android.os.Environment;
-
-import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
-import com.nononsenseapps.filepicker.FilePickerActivity;
-import com.nononsenseapps.filepicker.Utils;
-
-import org.yuzu.yuzu_emu.activities.CustomFilePickerActivity;
-
-import java.io.File;
-import java.util.List;
-
public final class FileBrowserHelper {
- public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) {
- Intent i = new Intent(activity, CustomFilePickerActivity.class);
-
- i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
- i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
- i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
- i.putExtra(FilePickerActivity.EXTRA_START_PATH,
- Environment.getExternalStorageDirectory().getPath());
- i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
- i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
-
+ public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title) {
+ Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+ i.putExtra(Intent.EXTRA_TITLE, title);
activity.startActivityForResult(i, requestCode);
}
- public static void openFilePicker(FragmentActivity activity, int requestCode, int title,
- List<String> extensions, boolean allowMultiple) {
- Intent i = new Intent(activity, CustomFilePickerActivity.class);
-
- i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMultiple);
- i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
- i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE);
- i.putExtra(FilePickerActivity.EXTRA_START_PATH,
- Environment.getExternalStorageDirectory().getPath());
- i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
- i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
-
- activity.startActivityForResult(i, requestCode);
- }
-
- @Nullable
public static String getSelectedDirectory(Intent result) {
- // Use the provided utility method to parse the result
- List<Uri> files = Utils.getSelectedFilesFromResult(result);
- if (!files.isEmpty()) {
- File file = Utils.getFileForUri(files.get(0));
- return file.getAbsolutePath();
- }
-
- return null;
- }
-
- @Nullable
- public static String[] getSelectedFiles(Intent result) {
- // Use the provided utility method to parse the result
- List<Uri> files = Utils.getSelectedFilesFromResult(result);
- if (!files.isEmpty()) {
- String[] paths = new String[files.size()];
- for (int i = 0; i < files.size(); i++)
- paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath();
- return paths;
- }
-
- return null;
+ return result.getDataString();
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java
index 11d06c7ee..624fd4a88 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java
@@ -1,37 +1,261 @@
package org.yuzu.yuzu_emu.utils;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract;
+
+import androidx.annotation.Nullable;
+import androidx.documentfile.provider.DocumentFile;
+
+import org.yuzu.yuzu_emu.model.MinimalDocumentFile;
+
import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.List;
public class FileUtil {
- public static byte[] getBytesFromFile(File file) throws IOException {
- final long length = file.length();
+ static final String PATH_TREE = "tree";
+ static final String DECODE_METHOD = "UTF-8";
+ static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
+ static final String TEXT_PLAIN = "text/plain";
- // You cannot create an array using a long type.
- if (length > Integer.MAX_VALUE) {
- // File is too large
- throw new IOException("File is too large!");
+ /**
+ * Create a file from directory with filename.
+ * @param context Application context
+ * @param directory parent path for file.
+ * @param filename file display name.
+ * @return boolean
+ */
+ @Nullable
+ public static DocumentFile createFile(Context context, String directory, String filename) {
+ try {
+ Uri directoryUri = Uri.parse(directory);
+ DocumentFile parent = DocumentFile.fromTreeUri(context, directoryUri);
+ if (parent == null) return null;
+ filename = URLDecoder.decode(filename, DECODE_METHOD);
+ String mimeType = APPLICATION_OCTET_STREAM;
+ if (filename.endsWith(".txt")) {
+ mimeType = TEXT_PLAIN;
+ }
+ DocumentFile exists = parent.findFile(filename);
+ if (exists != null) return exists;
+ return parent.createFile(mimeType, filename);
+ } catch (Exception e) {
+ Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
}
+ return null;
+ }
- byte[] bytes = new byte[(int) length];
+ /**
+ * Create a directory from directory with filename.
+ * @param context Application context
+ * @param directory parent path for directory.
+ * @param directoryName directory display name.
+ * @return boolean
+ */
+ @Nullable
+ public static DocumentFile createDir(Context context, String directory, String directoryName) {
+ try {
+ Uri directoryUri = Uri.parse(directory);
+ DocumentFile parent = DocumentFile.fromTreeUri(context, directoryUri);
+ if (parent == null) return null;
+ directoryName = URLDecoder.decode(directoryName, DECODE_METHOD);
+ DocumentFile isExist = parent.findFile(directoryName);
+ if (isExist != null) return isExist;
+ return parent.createDirectory(directoryName);
+ } catch (Exception e) {
+ Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
+ }
+ return null;
+ }
- int offset = 0;
- int numRead;
+ /**
+ * Open content uri and return file descriptor to JNI.
+ * @param context Application context
+ * @param path Native content uri path
+ * @param openmode will be one of "r", "r", "rw", "wa", "rwa"
+ * @return file descriptor
+ */
+ public static int openContentUri(Context context, String path, String openmode) {
+ try {
+ Uri uri = Uri.parse(path);
+ ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, openmode);
+ if (parcelFileDescriptor == null) {
+ Log.error("[FileUtil]: Cannot get the file descriptor from uri: " + path);
+ return -1;
+ }
+ return parcelFileDescriptor.detachFd();
+ }
+ catch (Exception e) {
+ Log.error("[FileUtil]: Cannot open content uri, error: " + e.getMessage());
+ }
+ return -1;
+ }
- try (InputStream is = new FileInputStream(file)) {
- while (offset < bytes.length
- && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
- offset += numRead;
+ /**
+ * Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
+ * This function will be faster than DoucmentFile.listFiles
+ * @param context Application context
+ * @param uri Directory uri.
+ * @return CheapDocument lists.
+ */
+ public static MinimalDocumentFile[] listFiles(Context context, Uri uri) {
+ final ContentResolver resolver = context.getContentResolver();
+ final String[] columns = new String[]{
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ DocumentsContract.Document.COLUMN_MIME_TYPE,
+ };
+ Cursor c = null;
+ final List<MinimalDocumentFile> results = new ArrayList<>();
+ try {
+ String docId;
+ if (isRootTreeUri(uri)) {
+ docId = DocumentsContract.getTreeDocumentId(uri);
+ } else {
+ docId = DocumentsContract.getDocumentId(uri);
+ }
+ final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId);
+ c = resolver.query(childrenUri, columns, null, null, null);
+ while(c.moveToNext()) {
+ final String documentId = c.getString(0);
+ final String documentName = c.getString(1);
+ final String documentMimeType = c.getString(2);
+ final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId);
+ MinimalDocumentFile document = new MinimalDocumentFile(documentName, documentMimeType, documentUri);
+ results.add(document);
}
+ } catch (Exception e) {
+ Log.error("[FileUtil]: Cannot list file error: " + e.getMessage());
+ } finally {
+ closeQuietly(c);
+ }
+ return results.toArray(new MinimalDocumentFile[0]);
+ }
+
+ /**
+ * Check whether given path exists.
+ * @param path Native content uri path
+ * @return bool
+ */
+ public static boolean Exists(Context context, String path) {
+ Cursor c = null;
+ try {
+ Uri mUri = Uri.parse(path);
+ final String[] columns = new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID };
+ c = context.getContentResolver().query(mUri, columns, null, null, null);
+ return c.getCount() > 0;
+ } catch (Exception e) {
+ Log.info("[FileUtil] Cannot find file from given path, error: " + e.getMessage());
+ } finally {
+ closeQuietly(c);
}
+ return false;
+ }
+
+ /**
+ * Check whether given path is a directory
+ * @param path content uri path
+ * @return bool
+ */
+ public static boolean isDirectory(Context context, String path) {
+ final ContentResolver resolver = context.getContentResolver();
+ final String[] columns = new String[] {
+ DocumentsContract.Document.COLUMN_MIME_TYPE
+ };
+ boolean isDirectory = false;
+ Cursor c = null;
+ try {
+ Uri mUri = Uri.parse(path);
+ c = resolver.query(mUri, columns, null, null, null);
+ c.moveToNext();
+ final String mimeType = c.getString(0);
+ isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
+ } catch (Exception e) {
+ Log.error("[FileUtil]: Cannot list files, error: " + e.getMessage());
+ } finally {
+ closeQuietly(c);
+ }
+ return isDirectory;
+ }
- // Ensure all the bytes have been read in
- if (offset < bytes.length) {
- throw new IOException("Could not completely read file " + file.getName());
+ /**
+ * Get file display name from given path
+ * @param path content uri path
+ * @return String display name
+ */
+ public static String getFilename(Context context, String path) {
+ final ContentResolver resolver = context.getContentResolver();
+ final String[] columns = new String[] {
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME
+ };
+ String filename = "";
+ Cursor c = null;
+ try {
+ Uri mUri = Uri.parse(path);
+ c = resolver.query(mUri, columns, null, null, null);
+ c.moveToNext();
+ filename = c.getString(0);
+ } catch (Exception e) {
+ Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
+ } finally {
+ closeQuietly(c);
}
+ return filename;
+ }
+
+ public static String[] getFilesName(Context context, String path) {
+ Uri uri = Uri.parse(path);
+ List<String> files = new ArrayList<>();
+ for (MinimalDocumentFile file: FileUtil.listFiles(context, uri)) {
+ files.add(file.getFilename());
+ }
+ return files.toArray(new String[0]);
+ }
- return bytes;
+ /**
+ * Get file size from given path.
+ * @param path content uri path
+ * @return long file size
+ */
+ public static long getFileSize(Context context, String path) {
+ final ContentResolver resolver = context.getContentResolver();
+ final String[] columns = new String[] {
+ DocumentsContract.Document.COLUMN_SIZE
+ };
+ long size = 0;
+ Cursor c =null;
+ try {
+ Uri mUri = Uri.parse(path);
+ c = resolver.query(mUri, columns, null, null, null);
+ c.moveToNext();
+ size = c.getLong(0);
+ } catch (Exception e) {
+ Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
+ } finally {
+ closeQuietly(c);
+ }
+ return size;
+ }
+
+ public static boolean isRootTreeUri(Uri uri) {
+ final List<String> paths = uri.getPathSegments();
+ return paths.size() == 2 && PATH_TREE.equals(paths.get(0));
+ }
+
+ public static void closeQuietly(AutoCloseable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (RuntimeException rethrown) {
+ throw rethrown;
+ } catch (Exception ignored) {
+ }
+ }
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java
deleted file mode 100644
index 2eb200da4..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.yuzu.yuzu_emu.utils;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Build;
-
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.FragmentActivity;
-
-import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
-
-public class PermissionsHandler {
- public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
-
- // We use permissions acceptance as an indicator if this is a first boot for the user.
- public static boolean isFirstBoot(final FragmentActivity activity) {
- return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED;
- }
-
- @TargetApi(Build.VERSION_CODES.M)
- public static boolean checkWritePermission(final FragmentActivity activity) {
- if (isFirstBoot(activity)) {
- activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
- REQUEST_CODE_WRITE_PERMISSION);
- return false;
- }
-
- return true;
- }
-
- public static boolean hasWriteAccess(Context context) {
- return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
- }
-}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java
index 5d22e8e08..6d3e58e18 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java
@@ -1,44 +1,38 @@
package org.yuzu.yuzu_emu.utils;
-import android.content.Intent;
-import android.os.Bundle;
-import android.text.TextUtils;
-
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.FragmentActivity;
import org.yuzu.yuzu_emu.R;
-import org.yuzu.yuzu_emu.activities.EmulationActivity;
+import org.yuzu.yuzu_emu.YuzuApplication;
+import org.yuzu.yuzu_emu.ui.main.MainActivity;
+import org.yuzu.yuzu_emu.ui.main.MainPresenter;
public final class StartupHandler {
- private static void handlePermissionsCheck(FragmentActivity parent) {
- // Ask the user to grant write permission if it's not already granted
- PermissionsHandler.checkWritePermission(parent);
+ private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext());
- String start_file = "";
- Bundle extras = parent.getIntent().getExtras();
- if (extras != null) {
- start_file = extras.getString("AutoStartFile");
- }
+ private static void handleStartupPromptDismiss(MainActivity parent) {
+ parent.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
+ }
- if (!TextUtils.isEmpty(start_file)) {
- // Start the emulation activity, send the ISO passed in and finish the main activity
- Intent emulation_intent = new Intent(parent, EmulationActivity.class);
- emulation_intent.putExtra("SelectedGame", start_file);
- parent.startActivity(emulation_intent);
- parent.finish();
- }
+ private static void markFirstBoot() {
+ final SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putBoolean("FirstApplicationLaunch", false);
+ editor.apply();
}
- public static void HandleInit(FragmentActivity parent) {
- if (PermissionsHandler.isFirstBoot(parent)) {
+ public static void handleInit(MainActivity parent) {
+ if (mPreferences.getBoolean("FirstApplicationLaunch", true)) {
+ markFirstBoot();
+
// Prompt user with standard first boot disclaimer
new AlertDialog.Builder(parent)
.setTitle(R.string.app_name)
.setIcon(R.mipmap.ic_launcher)
.setMessage(parent.getResources().getString(R.string.app_disclaimer))
.setPositiveButton(android.R.string.ok, null)
- .setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent))
+ .setOnDismissListener(dialogInterface -> handleStartupPromptDismiss(parent))
.show();
}
}
diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp
index 326dab5fc..0a3cb9162 100644
--- a/src/android/app/src/main/jni/config.cpp
+++ b/src/android/app/src/main/jni/config.cpp
@@ -18,11 +18,8 @@
namespace FS = Common::FS;
-const std::filesystem::path default_config_path =
- FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini";
-
Config::Config(std::optional<std::filesystem::path> config_path)
- : config_loc{config_path.value_or(default_config_path)},
+ : config_loc{config_path.value_or(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini")},
config{std::make_unique<INIReader>(FS::PathToUTF8String(config_loc))} {
Reload();
}
@@ -66,8 +63,8 @@ void Config::ReadSetting(const std::string& group, Settings::Setting<bool>& sett
template <typename Type, bool ranged>
void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
- setting = static_cast<Type>(config->GetInteger(group, setting.GetLabel(),
- static_cast<long>(setting.GetDefault())));
+ setting = static_cast<Type>(
+ config->GetInteger(group, setting.GetLabel(), static_cast<long>(setting.GetDefault())));
}
void Config::ReadValues() {
@@ -93,9 +90,9 @@ void Config::ReadValues() {
for (int i = 0; i < num_touch_from_button_maps; ++i) {
Settings::TouchFromButtonMap map;
map.name = config->Get("ControlsGeneral",
- std::string("touch_from_button_maps_") + std::to_string(i) +
- std::string("_name"),
- "default");
+ std::string("touch_from_button_maps_") + std::to_string(i) +
+ std::string("_name"),
+ "default");
const int num_touch_maps = config->GetInteger(
"ControlsGeneral",
std::string("touch_from_button_maps_") + std::to_string(i) + std::string("_count"),
@@ -105,9 +102,9 @@ void Config::ReadValues() {
for (int j = 0; j < num_touch_maps; ++j) {
std::string touch_mapping =
config->Get("ControlsGeneral",
- std::string("touch_from_button_maps_") + std::to_string(i) +
- std::string("_bind_") + std::to_string(j),
- "");
+ std::string("touch_from_button_maps_") + std::to_string(i) +
+ std::string("_bind_") + std::to_string(j),
+ "");
map.buttons.emplace_back(std::move(touch_mapping));
}
@@ -127,16 +124,16 @@ void Config::ReadValues() {
ReadSetting("Data Storage", Settings::values.use_virtual_sd);
FS::SetYuzuPath(FS::YuzuPath::NANDDir,
config->Get("Data Storage", "nand_directory",
- FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
+ FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
FS::SetYuzuPath(FS::YuzuPath::SDMCDir,
config->Get("Data Storage", "sdmc_directory",
- FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
+ FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
FS::SetYuzuPath(FS::YuzuPath::LoadDir,
config->Get("Data Storage", "load_directory",
- FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
+ FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
FS::SetYuzuPath(FS::YuzuPath::DumpDir,
config->Get("Data Storage", "dump_directory",
- FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
+ FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
ReadSetting("Data Storage", Settings::values.gamecard_inserted);
ReadSetting("Data Storage", Settings::values.gamecard_current_game);
ReadSetting("Data Storage", Settings::values.gamecard_path);
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
index 2955122be..8f085798d 100644
--- a/src/android/app/src/main/jni/id_cache.cpp
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -1,9 +1,17 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <jni.h>
+
+#include "common/fs/fs_android.h"
#include "jni/id_cache.h"
static JavaVM* s_java_vm;
static jclass s_native_library_class;
static jmethodID s_exit_emulation_activity;
+static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
+
namespace IDCache {
JNIEnv* GetEnvForThread() {
@@ -34,3 +42,41 @@ jmethodID GetExitEmulationActivity() {
}
} // namespace IDCache
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+jint JNI_OnLoad(JavaVM* vm, void* reserved) {
+ s_java_vm = vm;
+
+ JNIEnv* env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK)
+ return JNI_ERR;
+
+ // Initialize Java classes
+ const jclass native_library_class = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary");
+ s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
+ s_exit_emulation_activity =
+ env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
+
+ // Initialize Android Storage
+ Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
+
+ return JNI_VERSION;
+}
+
+void JNI_OnUnload(JavaVM* vm, void* reserved) {
+ JNIEnv* env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK) {
+ return;
+ }
+
+ // UnInitialize Android Storage
+ Common::FS::Android::UnRegisterCallbacks();
+ env->DeleteGlobalRef(s_native_library_class);
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index f0df6cac1..c1880db46 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
#include <codecvt>
#include <locale>
#include <string>
@@ -7,6 +10,7 @@
#include <android/native_window_jni.h>
#include "common/detached_tasks.h"
+#include "common/fs/path_util.h"
#include "common/logging/backend.h"
#include "common/logging/log.h"
#include "common/microprofile.h"
@@ -257,9 +261,11 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(JNIEnv* env,
jint layout_option,
jint rotation) {}
-void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserDirectory([[maybe_unused]] JNIEnv* env,
- [[maybe_unused]] jclass clazz,
- [[maybe_unused]] jstring j_directory) {}
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory([[maybe_unused]] JNIEnv* env,
+ [[maybe_unused]] jclass clazz,
+ [[maybe_unused]] jstring j_directory) {
+ Common::FS::SetAppDirectory(GetJString(env, j_directory));
+}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz) {}
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h
index 3b23f380b..fbe015b55 100644
--- a/src/android/app/src/main/jni/native.h
+++ b/src/android/app/src/main/jni/native.h
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
#pragma once
#include <jni.h>
@@ -8,16 +11,16 @@ extern "C" {
#endif
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env,
- jclass clazz);
+ jclass clazz);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIEnv* env,
- jclass clazz);
+ jclass clazz);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env,
- jclass clazz);
+ jclass clazz);
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env,
- jclass clazz);
+ jclass clazz);
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadEvent(
JNIEnv* env, jclass clazz, jstring j_device, jint j_button, jint action);
@@ -29,61 +32,58 @@ JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadAxisEv
JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val);
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JNIEnv* env,
- jclass clazz,
- jfloat x, jfloat y,
- jboolean pressed);
+ jclass clazz,
+ jfloat x, jfloat y,
+ jboolean pressed);
-JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env,
- jclass clazz, jfloat x,
- jfloat y);
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz,
+ jfloat x, jfloat y);
-JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env,
- jclass clazz,
- jstring j_file);
+JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env, jclass clazz,
+ jstring j_file);
-JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env,
- jclass clazz,
- jstring j_filename);
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, jclass clazz,
+ jstring j_filename);
-JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription(
- JNIEnv* env, jclass clazz, jstring j_filename);
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription(JNIEnv* env,
+ jclass clazz,
+ jstring j_filename);
-JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId(JNIEnv* env,
- jclass clazz,
- jstring j_filename);
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId(JNIEnv* env, jclass clazz,
+ jstring j_filename);
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetRegions(JNIEnv* env,
- jclass clazz,
- jstring j_filename);
+ jclass clazz,
+ jstring j_filename);
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany(JNIEnv* env,
- jclass clazz,
- jstring j_filename);
+ jclass clazz,
+ jstring j_filename);
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision(JNIEnv* env,
- jclass clazz);
+ jclass clazz);
-JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserDirectory(
- JNIEnv* env, jclass clazz, jstring j_directory);
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory(JNIEnv* env,
+ jclass clazz,
+ jstring j_directory);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory(
JNIEnv* env, jclass clazz, jstring path_);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env,
- jclass clazz,
- jstring path);
+ jclass clazz,
+ jstring path);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env,
- jclass clazz);
+ jclass clazz);
JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
- jclass clazz);
-JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env,
- jclass clazz,
- jboolean enable);
+ jclass clazz);
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env, jclass clazz,
+ jboolean enable);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env,
- jclass clazz);
+ jclass clazz);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(
JNIEnv* env, jclass clazz, jint layout_option, jint rotation);
@@ -96,18 +96,17 @@ Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_
JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env,
- jclass clazz,
- jobject surf);
+ jclass clazz,
+ jobject surf);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env,
- jclass clazz);
+ jclass clazz);
-JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env,
- jclass clazz,
- jstring j_game_id);
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env, jclass clazz,
+ jstring j_game_id);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings(JNIEnv* env,
- jclass clazz);
+ jclass clazz);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserSetting(
JNIEnv* env, jclass clazz, jstring j_game_id, jstring j_section, jstring j_key,
@@ -117,10 +116,10 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetUserSetting(
JNIEnv* env, jclass clazz, jstring game_id, jstring section, jstring key);
JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats(JNIEnv* env,
- jclass clazz);
+ jclass clazz);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env,
- jclass clazz);
+ jclass clazz);
#ifdef __cplusplus
}
diff --git a/src/android/app/src/main/res/layout/filepicker_toolbar.xml b/src/android/app/src/main/res/layout/filepicker_toolbar.xml
deleted file mode 100644
index 644934171..000000000
--- a/src/android/app/src/main/res/layout/filepicker_toolbar.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/nnf_picker_toolbar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:background="?attr/colorPrimary"
- android:minHeight="?attr/actionBarSize"
- android:theme="?nnf_toolbarTheme">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
-
- <TextView
- android:id="@+id/filepicker_title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:ellipsize="start"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
-
- <TextView
- android:id="@+id/nnf_current_dir"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:ellipsize="start"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle" />
- </LinearLayout>
-</androidx.appcompat.widget.Toolbar>
diff --git a/src/android/app/src/main/res/values-night/styles_filepicker.xml b/src/android/app/src/main/res/values-night/styles_filepicker.xml
deleted file mode 100644
index 1a175cdcf..000000000
--- a/src/android/app/src/main/res/values-night/styles_filepicker.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
- <style name="FilePickerBaseTheme" parent="NNF_BaseTheme" />
-</resources>
diff --git a/src/android/app/src/main/res/values-w1050dp/dimens.xml b/src/android/app/src/main/res/values-w1050dp/dimens.xml
index 92fcb2b66..78481cb1c 100644
--- a/src/android/app/src/main/res/values-w1050dp/dimens.xml
+++ b/src/android/app/src/main/res/values-w1050dp/dimens.xml
@@ -2,5 +2,4 @@
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 1024dp of available width. -->
- <dimen name="activity_horizontal_margin">96dp</dimen>
</resources>
diff --git a/src/android/app/src/main/res/values-w820dp/dimens.xml b/src/android/app/src/main/res/values-w820dp/dimens.xml
index d27181e85..1b1ada235 100644
--- a/src/android/app/src/main/res/values-w820dp/dimens.xml
+++ b/src/android/app/src/main/res/values-w820dp/dimens.xml
@@ -1,5 +1,4 @@
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 820dp of available width. -->
- <dimen name="activity_horizontal_margin">64dp</dimen>
</resources>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index cc84f700e..893f6aa1a 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -48,7 +48,7 @@
<string name="grid_menu_core_settings">Settings</string>
<!-- Add Directory Screen-->
- <string name="select_game_folder">Select Game Folder</string>
+ <string name="select_game_folder">Select game folder</string>
<string name="install_cia_title">Install CIA</string>
<!-- Preferences Screen -->
@@ -71,7 +71,6 @@
<string name="emulation_touch_overlay_reset">Reset Overlay</string>
<string name="emulation_close_game_message">Are you sure that you would like to close the current game?</string>
- <string name="write_permission_needed">You need to allow write access to external storage for the emulator to work</string>
<string name="load_settings">Loading Settings...</string>
<string name="external_storage_not_mounted">The external storage needs to be available in order to use yuzu</string>
diff --git a/src/android/app/src/main/res/values/styles.xml b/src/android/app/src/main/res/values/styles.xml
index 62f24bad3..fdedc9b2e 100644
--- a/src/android/app/src/main/res/values/styles.xml
+++ b/src/android/app/src/main/res/values/styles.xml
@@ -61,22 +61,6 @@
<item name="android:windowAllowReturnTransitionOverlap">true</item>
</style>
- <!-- Inherit from a base file picker theme that handles day/night -->
- <style name="FilePickerTheme" parent="FilePickerBaseTheme">
- <item name="colorSurface">@color/view_background</item>
- <item name="colorOnSurface">@color/view_text</item>
- <item name="colorPrimary">@color/citra_orange</item>
- <item name="colorPrimaryDark">@color/citra_orange_dark</item>
- <item name="colorAccent">@color/citra_accent</item>
- <item name="android:windowBackground">@color/view_background</item>
-
- <!-- Need to set this also to style create folder dialog -->
- <item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
-
- <item name="nnf_list_item_divider">@drawable/gamelist_divider</item>
- <item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.DayNight.ActionBar</item>
- </style>
-
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.DayNight.Dialog.Alert">
<item name="colorSurface">@color/view_background</item>
<item name="colorOnSurface">@color/view_text</item>
diff --git a/src/android/app/src/main/res/values/styles_filepicker.xml b/src/android/app/src/main/res/values/styles_filepicker.xml
deleted file mode 100644
index 0b0c3fe1a..000000000
--- a/src/android/app/src/main/res/values/styles_filepicker.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
- <style name="FilePickerBaseTheme" parent="NNF_BaseTheme.Light" />
-</resources>
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 13ed68b3f..aecb46872 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -155,6 +155,14 @@ if (WIN32)
target_link_libraries(common PRIVATE ntdll)
endif()
+if(ANDROID)
+ target_sources(common
+ PRIVATE
+ fs/fs_android.cpp
+ fs/fs_android.h
+ )
+endif()
+
if(ARCHITECTURE_x86_64)
target_sources(common
PRIVATE
diff --git a/src/common/fs/file.cpp b/src/common/fs/file.cpp
index 656b03cc5..b0b25eb43 100644
--- a/src/common/fs/file.cpp
+++ b/src/common/fs/file.cpp
@@ -5,6 +5,9 @@
#include "common/fs/file.h"
#include "common/fs/fs.h"
+#ifdef ANDROID
+#include "common/fs/fs_android.h"
+#endif
#include "common/logging/log.h"
#ifdef _WIN32
@@ -252,6 +255,23 @@ void IOFile::Open(const fs::path& path, FileAccessMode mode, FileType type, File
} else {
_wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type));
}
+#elif ANDROID
+ if (Android::IsContentUri(path)) {
+ ASSERT_MSG(mode == FileAccessMode::Read, "Content URI file access is for read-only!");
+ const auto fd = Android::OpenContentUri(path, Android::OpenMode::Read);
+ if (fd != -1) {
+ file = fdopen(fd, "r");
+ const auto error_num = errno;
+ if (error_num != 0 && file == nullptr) {
+ LOG_ERROR(Common_Filesystem, "Error opening file: {}, error: {}", path.c_str(),
+ strerror(error_num));
+ }
+ } else {
+ LOG_ERROR(Common_Filesystem, "Error opening file: {}", path.c_str());
+ }
+ } else {
+ file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
+ }
#else
file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
#endif
@@ -372,6 +392,23 @@ u64 IOFile::GetSize() const {
// Flush any unwritten buffered data into the file prior to retrieving the file size.
std::fflush(file);
+#if ANDROID
+ u64 file_size = 0;
+ if (Android::IsContentUri(file_path)) {
+ file_size = Android::GetSize(file_path);
+ } else {
+ std::error_code ec;
+
+ file_size = fs::file_size(file_path, ec);
+
+ if (ec) {
+ LOG_ERROR(Common_Filesystem,
+ "Failed to retrieve the file size of path={}, ec_message={}",
+ PathToUTF8String(file_path), ec.message());
+ return 0;
+ }
+ }
+#else
std::error_code ec;
const auto file_size = fs::file_size(file_path, ec);
@@ -381,6 +418,7 @@ u64 IOFile::GetSize() const {
PathToUTF8String(file_path), ec.message());
return 0;
}
+#endif
return file_size;
}
diff --git a/src/common/fs/fs_android.cpp b/src/common/fs/fs_android.cpp
new file mode 100644
index 000000000..298a79bac
--- /dev/null
+++ b/src/common/fs/fs_android.cpp
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/fs/fs_android.h"
+
+namespace Common::FS::Android {
+
+JNIEnv* GetEnvForThread() {
+ thread_local static struct OwnedEnv {
+ OwnedEnv() {
+ status = g_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
+ if (status == JNI_EDETACHED)
+ g_jvm->AttachCurrentThread(&env, nullptr);
+ }
+
+ ~OwnedEnv() {
+ if (status == JNI_EDETACHED)
+ g_jvm->DetachCurrentThread();
+ }
+
+ int status;
+ JNIEnv* env = nullptr;
+ } owned;
+ return owned.env;
+}
+
+void RegisterCallbacks(JNIEnv* env, jclass clazz) {
+ env->GetJavaVM(&g_jvm);
+ native_library = clazz;
+
+#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \
+ F(JMethodID, JMethodName, Signature)
+#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) \
+ F(JMethodID, JMethodName, Signature)
+#define F(JMethodID, JMethodName, Signature) \
+ JMethodID = env->GetStaticMethodID(native_library, JMethodName, Signature);
+ ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
+ ANDROID_STORAGE_FUNCTIONS(FS)
+#undef F
+#undef FS
+#undef FR
+}
+
+void UnRegisterCallbacks() {
+#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
+#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) F(JMethodID)
+#define F(JMethodID) JMethodID = nullptr;
+ ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
+ ANDROID_STORAGE_FUNCTIONS(FS)
+#undef F
+#undef FS
+#undef FR
+}
+
+bool IsContentUri(const std::string& path) {
+ constexpr std::string_view prefix = "content://";
+ if (path.size() < prefix.size()) [[unlikely]] {
+ return false;
+ }
+
+ return path.find(prefix) == 0;
+}
+
+int OpenContentUri(const std::string& filepath, OpenMode openmode) {
+ if (open_content_uri == nullptr)
+ return -1;
+
+ const char* mode = "";
+ switch (openmode) {
+ case OpenMode::Read:
+ mode = "r";
+ break;
+ default:
+ UNIMPLEMENTED();
+ return -1;
+ }
+ auto env = GetEnvForThread();
+ jstring j_filepath = env->NewStringUTF(filepath.c_str());
+ jstring j_mode = env->NewStringUTF(mode);
+ return env->CallStaticIntMethod(native_library, open_content_uri, j_filepath, j_mode);
+}
+
+#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \
+ F(FunctionName, ReturnValue, JMethodID, Caller)
+#define F(FunctionName, ReturnValue, JMethodID, Caller) \
+ ReturnValue FunctionName(const std::string& filepath) { \
+ if (JMethodID == nullptr) { \
+ return 0; \
+ } \
+ auto env = GetEnvForThread(); \
+ jstring j_filepath = env->NewStringUTF(filepath.c_str()); \
+ return env->Caller(native_library, JMethodID, j_filepath); \
+ }
+ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
+#undef F
+#undef FR
+
+} // namespace Common::FS::Android
diff --git a/src/common/fs/fs_android.h b/src/common/fs/fs_android.h
new file mode 100644
index 000000000..bb8a52648
--- /dev/null
+++ b/src/common/fs/fs_android.h
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <string>
+#include <vector>
+#include <jni.h>
+
+#define ANDROID_STORAGE_FUNCTIONS(V) \
+ V(OpenContentUri, int, (const std::string& filepath, OpenMode openmode), open_content_uri, \
+ "openContentUri", "(Ljava/lang/String;Ljava/lang/String;)I")
+
+#define ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(V) \
+ V(GetSize, std::uint64_t, get_size, CallStaticLongMethod, "getSize", "(Ljava/lang/String;)J")
+
+namespace Common::FS::Android {
+
+static JavaVM* g_jvm = nullptr;
+static jclass native_library = nullptr;
+
+#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
+#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) F(JMethodID)
+#define F(JMethodID) static jmethodID JMethodID = nullptr;
+ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
+ANDROID_STORAGE_FUNCTIONS(FS)
+#undef F
+#undef FS
+#undef FR
+
+enum class OpenMode {
+ Read,
+ Write,
+ ReadWrite,
+ WriteAppend,
+ WriteTruncate,
+ ReadWriteAppend,
+ ReadWriteTruncate,
+ Never
+};
+
+void RegisterCallbacks(JNIEnv* env, jclass clazz);
+
+void UnRegisterCallbacks();
+
+bool IsContentUri(const std::string& path);
+
+#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) \
+ F(FunctionName, Parameters, ReturnValue)
+#define F(FunctionName, Parameters, ReturnValue) ReturnValue FunctionName Parameters;
+ANDROID_STORAGE_FUNCTIONS(FS)
+#undef F
+#undef FS
+
+#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \
+ F(FunctionName, ReturnValue)
+#define F(FunctionName, ReturnValue) ReturnValue FunctionName(const std::string& filepath);
+ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
+#undef F
+#undef FR
+
+} // namespace Common::FS::Android
diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp
index ca755b053..e026a13d9 100644
--- a/src/common/fs/path_util.cpp
+++ b/src/common/fs/path_util.cpp
@@ -6,6 +6,9 @@
#include <unordered_map>
#include "common/fs/fs.h"
+#ifdef ANDROID
+#include "common/fs/fs_android.h"
+#endif
#include "common/fs/fs_paths.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"
@@ -80,9 +83,7 @@ public:
yuzu_paths.insert_or_assign(yuzu_path, new_path);
}
-private:
- PathManagerImpl() {
- fs::path yuzu_path;
+ void Reinitialize(fs::path yuzu_path = {}) {
fs::path yuzu_path_cache;
fs::path yuzu_path_config;
@@ -96,12 +97,9 @@ private:
yuzu_path_cache = yuzu_path / CACHE_DIR;
yuzu_path_config = yuzu_path / CONFIG_DIR;
#elif ANDROID
- // On Android internal storage is mounted as "/sdcard"
- if (Exists("/sdcard")) {
- yuzu_path = "/sdcard/yuzu-emu";
- yuzu_path_cache = yuzu_path / CACHE_DIR;
- yuzu_path_config = yuzu_path / CONFIG_DIR;
- }
+ ASSERT(!yuzu_path.empty());
+ yuzu_path_cache = yuzu_path / CACHE_DIR;
+ yuzu_path_config = yuzu_path / CONFIG_DIR;
#else
yuzu_path = GetCurrentDir() / PORTABLE_DIR;
@@ -129,6 +127,11 @@ private:
GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR);
}
+private:
+ PathManagerImpl() {
+ Reinitialize();
+ }
+
~PathManagerImpl() = default;
void GenerateYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) {
@@ -217,6 +220,10 @@ fs::path RemoveTrailingSeparators(const fs::path& path) {
return fs::path{string_path};
}
+void SetAppDirectory(const std::string& app_directory) {
+ PathManagerImpl::GetInstance().Reinitialize(app_directory);
+}
+
const fs::path& GetYuzuPath(YuzuPath yuzu_path) {
return PathManagerImpl::GetInstance().GetYuzuPathImpl(yuzu_path);
}
@@ -357,6 +364,12 @@ std::vector<std::string> SplitPathComponents(std::string_view filename) {
std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) {
std::string path(path_);
+#ifdef ANDROID
+ if (Android::IsContentUri(path)) {
+ return path;
+ }
+#endif // ANDROID
+
char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/';
diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h
index 13d713f1e..7cfe85b70 100644
--- a/src/common/fs/path_util.h
+++ b/src/common/fs/path_util.h
@@ -181,6 +181,14 @@ template <typename Path>
#endif
/**
+ * Sets the directory used for application storage. Used on Android where we do not know internal
+ * storage until informed by the frontend.
+ *
+ * @param app_directory Directory to use for application storage.
+ */
+void SetAppDirectory(const std::string& app_directory);
+
+/**
* Gets the filesystem path associated with the YuzuPath enum.
*
* @param yuzu_path YuzuPath enum